<?php
/**
 * Spaph/Controller.php
 */
class Spaph_Controller
{
    /**
     * This like will be called only by Spaph_Controller during
     * bootsrapping or by Spaph throughout the application.
     *
     * @return object
     */
    public static function getInstance()
    {
        if(!isset(Spaph::$_instance)) {
            Spaph::$_instance = new Spaph();
        }

        return Spaph::$_instance;
    }

    // Every controller should get constructed only once.
    protected function __construct() {}

    // Every controller has an opportunity to override _init.
    protected function _init() {}
}

class Spaph extends Spaph_Controller
{
    protected static $_instance;
    private $_controller          = null;
    private $_action              = null;
    private $_dispatch            = true;
    private $_forward             = array();
    private $_redirect            = '';
    private $_controller_obj      = array(); // keep track of objects
    private $_controller_init     = array(); // keep track of controller::_init
    private $_controller_dir      = '';
    private $_plugin              = array();
    private $_plugin_idx          = array();
    private $_base_url            = null;
    private $_action_param        = 'do';
    private $_config              = array();
    private $_host                = null;
    private $_dbh                 = null;
    private $_map                 = array();
    private $_running             = false;   // run() will flip this
    private $_selfUrl             = '';
    public  static $Reg           = array(); // Registry for storing global values



    /**
     * Start timer. Multiple timers can be had by naming them.
     *
     * @param string $name
     * @return float
     */
    public static function timer_start($name='default')
    {
       $name .= '__timer_start_'.$name;
       Spaph::$Reg[$name] = microtime(true);
       return Spaph::$Reg[$name];
    }

    /**
     * Stop the timer and return the difference, now - start.
     *
     * @param string $name
     * @return float
     */
    public static function timer_stop($name='default')
    {
       $name .= '__timer_start_'.$name;
       return microtime(true) - Spaph::$Reg[$name];
    }

    /**
     * var_export a variable as preformatted html.
     *
     * @input mixed $data
     * @return string
     */
    public static function dump($data)
    {
        return sprintf('<pre class="spaph-dump">%s</pre>',htmlspecialchars(var_export($data,true)));
    }

    /**
     * This is, admittedly, a naive attempt at creating a uuid method.
     * If there are major holes in this method, then I hope somebody
     * will call it to my attention. Otherwise, enjoy.
     *
     * @param $algo md5, sha1 
     * @param $salt
     * @param $raw set to TRUE outputs raw binary data
     * @return string
     */
    public static function uuid($algo='md5',$salt='',$raw=false)
    {
        $alphanum = 'abcdefghijkmnpqrstuvwxyz0123456789~`!@#$%^&*-_+=<>~';
        $string   = $salt ? $salt : mt_rand();

        for($i=0; $i<6; $i++) {
            $string .= $alphanum[ mt_rand(0,50) ];
        }

        $string .= microtime(true).mt_rand();

        return hash($algo, $string, $raw);
    }

    /**
     * This is from memic post at php.net uniqid
     *
     * @param string $separate 
     * @return string
     */
    public static function uuid4($separate='')
    {
        return sprintf( "%04x%04x$separate%04x$separate%04x$separate%04x$separate%04x%04x%04x",
            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
            mt_rand( 0, 0x0fff ) | 0x4000,
            mt_rand( 0, 0x3fff ) | 0x8000,
            mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) );
    }
    
    public function request()
    {
        return new Spaph_Request();
    }

    /**
     * Get or set self::$Reg. This is basically a simple Spaph Registry
     * to store global values within the scope of Spaph.
     */
    public function reg()
    {
        return new Spaph_Reg();
    }

    /**
     * This is a lazy session_start wrapper.
     *
     * @param string $name
     * @param boolean $force
     */
    public function session_start($name, $regenerate=0)
    {
        // session_start() has NOT been called yet
        if( '' == session_id()  ) {

            // and cookied and the cookie value looks like a hash (> 25)
            // because sometimes it's value is 'deleted'??
            // regenerate will force a session_start too.
            if( (isset($_COOKIE[$name]) && strlen($_COOKIE[$name]) > 25) || $regenerate ) {
                session_name($name);
                session_start();
            }
        }

        // The session must have been started by now
        if($regenerate) {
            session_regenerate_id(true);
        }
    }

    /**
     * This is a lazy session_write_close wrapper.
     */
    public function session_close()
    {
        // No session was started
        if('' == session_id()) {
            return;
        }

        session_write_close();
    }

    /**
     * Really, really destroy the session
     */
    public function session_destroy()
    {
        // Unset all of the session variables.
        $_SESSION = array();

        // Get session name
        $sess_name = session_name();

        // Delete cookie info.
        if (isset($_COOKIE[$sess_name])) {
            $ck = session_get_cookie_params();
            setcookie($sess_name, '', time()-2592000, $ck['path'], $ck['domain'], $ck['secure']);
            unset($_COOKIE[$sess_name]);
        }

        if('' != session_id()) {
            session_destroy();
        }
    }

    /**
     * Map do=somekeyword to controller.action.
     *
     * Set:
     *   $spaph->map(array('signin' => 'main.supersecretsignin', ...));
     *
     * Get:
     *   $action = $spaph->map('signin'); // main.supersecretsignin
     *
     * @param array $mapper
     * @return string
     */
    public function map($mapper = null)
    {
        if(isset($mapper)) {
            if(is_array($mapper)) {
                $this->_map = $mapper;
            }
            else if(is_string($mapper) && isset($this->_map[$mapper])) {
                return $this->_map[$mapper];
            }

            return;
        }

        return $this->_map;
    }

    /**
     * A friendly wrapper around Spaph_DB.
     *
     * @param string $adapter
     * @param array $config
     * @return Spaph_DB
     */
    public function dbh($adapter = null, $config = null)
    {
        if(isset($adapter))
            $this->_dbh = new Spaph_DB($adapter, $config);

        return $this->_dbh;
    }

    /**
     * Set the root directory of all controllers. This must be set in the
     * bootstrap file (index.php) before Spaph::run() is called, typically
     * immediately after requiring Spaph.php.
     *
     * setViewDir and setPluginDir can be called later to override what
     * setControllerDir does. Also, php's set_include_path can be reset
     * after setControllerDir gets its grubby hands on it, if needed.
     *
     * @param string $dir
     * @return null
     */
    public function setControllerDir($dir)
    {
        $this->_controller_dir = trim(rtrim($dir,'\\/'));

        // Break controller dir down
        $parts  = preg_split('/[\\\\\/]+/',$this->getControllerDir());
        unset($parts[count($parts)-1]);

        // set views and plugins root dir
        $this->setViewDir(implode(DIRECTORY_SEPARATOR,array_merge($parts,array('views'))));
        $this->setPluginDir(implode(DIRECTORY_SEPARATOR,array_merge($parts,array('plugins'))));

        // Add models, lib to path
        $path   = array();
        $path[] = '.';
        $path[] = implode(DIRECTORY_SEPARATOR,array_merge($parts,array('lib')));

        set_include_path( implode(PATH_SEPARATOR, $path) );
    }

    /**
     * Wrapper method.
     *
     * @param string $dir
     */
    public function setPluginDir($dir)
    {
        Spaph_Plugin::setPluginDir($dir);
    }

    /**
     * Wrapper method.
     *
     * @param string $dir
     */
    public function setViewDir($dir)
    {
        Spaph_View::setViewDir($dir);
    }

    /**
     * Set global variables for the view object.
     *
     * @param array $vars
     */
    public function setViewVars($vars)
    {
        Spaph_View::setViewVars($vars);
    }

    /**
     * Set the name of the query parameter that Spaph::route() will use
     * to figure out the user's requested controller.action.
     *
     * @param string $do
     * @return null
     */
    public function setActionParam($do = 'do')
    {
        $this->_action_param = $do;
    }

    /**
     * Get the name of the query parameter that Spaph::route() will use
     * to figure out the user's requested controller.action.
     *
     * @return string
     */
    public function getActionParam()
    {
        return $this->_action_param;
    }

    /**
     * Set host name of the application, www.somesite.com.
     *
     * @param string $host
     * @return null
     */
    public function setHost($host)
    {
        $this->_host = $host;
    }

    /**
     * Set the part of the url that appears after the host name.
     * /myapp/ishere
     *
     * So, glueing Spaph::_host and Spaph::_base_url will equal:
     * www.somesite.com/myapp/ishere
     *
     * @param string $url
     * @return null
     */
    public function setBaseUrl($url)
    {
        $this->_base_url = $url;
    }

    /**
     * Get the host name set by Spaph::setHost(). Otherwise, this method will
     * try to figure out the host name by inspecting $_SERVER['SCRIPT_URI'].
     *
     * @return string
     */
    public function getHost()
    {
        if(!isset($this->_host) && $_SERVER['SCRIPT_URI']) {
            $parts = parse_url($_SERVER['SCRIPT_URI']);
            $this->setHost($parts['host']);
        }

        return $this->_host;
    }

    /**
     * Get the base url set by Spaph::setBaseUrl(). Otherwise, this method will
     * try to figure out the base url by inspecting $_SERVER['PHP_SELF'].
     *
     * @return string
     */
    public function getBaseUrl()
    {
        if(!isset($this->_base_url))
            $this->_base_url = rtrim(substr( $_SERVER['PHP_SELF'], 0, -strlen(basename($_SERVER['PHP_SELF'])) ),'/');

        return $this->_base_url;
    }

    /**
     * Get and set selfUrl of the controller.
     *
     * @param string
     * @return string
     */
    public function selfUrl($url=null)
    {
        if(isset($url))
            $this->_selfUrl = $url;

        return $this->_selfUrl;
    }

    /**
     * Get the root directory of all controllers.
     *
     * @return string
     */
    public function getControllerDir()
    {
        $parts = preg_split('/[\\\\\/]+/',$this->_controller_dir);
        return implode(DIRECTORY_SEPARATOR,$parts);
    }

    /**
     * Set or get a config object
     *
     * @param Spaph_Conf $conf
     * @param string $name
     * @return object
     */
    public function config($conf=null, $name='default')
    {
        if(isset($conf) && is_object($conf)) {
            $this->_config[$name] = $conf;
        }

        return $this->_config[$name];
    }

    /**
     * Get a view object to render a template file
     *
     * @param object|string
     * @return object
     */
    public function view()
    {
        $tmp = array();
        for($i = 0 ; $i < func_num_args(); $i++) {
            $tmp[] = is_object(func_get_arg($i)) ? func_get_arg($i) : $this->getPlugin(func_get_arg($i));
        }

        return new Spaph_View($tmp);
    }

    /**
     * Set the requested action.
     *
     * Call this action *before* the controller's action is called. This can be
     * called in the bootstrap file (index.php) to set a default controller.action.
     *
     * This can be called with one parameter like setAction('controller.action')
     *
     * @param string $action
     * @param string $controller
     * @return null
     */
    public function setAction($action=null, $controller = null)
    {
        if(strpos($action, '.') !== false)
            list($controller, $action) = explode('.',$action);

        if(isset($controller) && $controller)
            $this->_controller = $controller;

        if(isset($action) && $action)
            $this->_action = $action;
    }

    /**
     * Get an array containing information about the requested action.
     *
     * @return array
     */
    public function getAction()
    {
        return array($this->_action, $this->_controller);
    }

    /**
     * Figure out what controller and action to run. What resource did the
     * user request? The route() method inspects the $_GET and $_POST
     * arrays for a key that matches Spaph::_action_param, 'do' by default.
     * So a query will have something like ?do=controller.action or
     * ?do=action1. route() will call Spaph::setAction() if an action
     * parameter is found.
     *
     * If action1 is discovered without a controller and there is a map set
     * for action1, then route will follow the value of map[ctrl.action1]. Set
     * mappings with Spaph::map();
     *
     * @return array
     */
    private function route()
    {
        // Set the _route according to the $_GET or $_POST request.
        $query = (isset($_GET[$this->_action_param])) ? $_GET[$this->_action_param] : '';
        $query = (isset($_POST[$this->_action_param])) ? $_POST[$this->_action_param] : $query;
        $route = $query ? explode('.',$query) : array();

        // Set action according to route. setAction($action,$controller)
        if(count($route) > 1) {
            $this->setAction($route[1],$route[0]);
        }
        elseif(count($route) > 0) {
            if($mapped = $this->map($route[0])) {
                $this->setAction($mapped);
            }
            else { $this->setAction($route[0], null); }
        }
        else {
            list($action, $controller) = $this->getAction();            // Use default action.
            if(($mapped = $this->map($action)) && is_null($controller)) // Check if default is a map.
                $this->setAction($mapped);
        }

        return $this->getAction();
    }

    /**
     * Register a plugin.
     *
     * @param string|array $file - Full path and filename
     * @return null
     */
    public function regPlugin($file)
    {
        $file = is_array($file) ? $file : array($file);

        // instantiate plugin objects
        foreach($file as $f) {
            $filename = Spaph_Plugin::$ROOT_DIR . trim($f) . '.php';
            require_once($filename);
            $className = basename($filename,'.php');
            $this->_plugin_idx[] = $className; // Remember plugin sequence because this plugin might call regPlugin too
            $this->_plugin[array_search($className, $this->_plugin_idx)] = new $className();
        }

        ksort($this->_plugin,SORT_NUMERIC);
    }

    /**
     * Unset a plugin.
     *
     * @param string $classname
     * @return null
     */
    public function unsetPlugin($classname)
    {
        // Look for the plugin named Class
        for($i=0;$i<count($this->_plugin);$i++) {
            if( strcasecmp($classname, get_class($this->_plugin[$i])) == 0 ) {
                unset($this->_plugin[$i]);
                return;
            }
        }
    }

    /**
     * Get a plugin object. Either specify a class or get an array of
     * all plugin objects.
     *
     * @param string $classname
     * @return object|array|null
     */
    public function getPlugin($classname = null)
    {
        // Look for the plugin named Class
        if(isset($classname)) {
            foreach($this->_plugin as $plugin) {
                if( strcasecmp($classname, get_class($plugin)) == 0 )
                    return $plugin;
            }

            return;
        }

        // Return all plugin objects.
        return $this->_plugin;
    }


    /**
     * Kick start this jalopy.
     *
     * 1. instantiate plugins
     * 2. route
     * 3. call plugin _preDispatch()
     * 4. call controller _init()
     * 5. call controller action
     * 6. call plugin _postDispatch()
     *
     * @return null
     */
    public function run()
    {
        // Already running, only call run() once.
        if($this->_running)
            return true;
            
        $controllerObject = null;
        $action           = null;
        $controller       = null;
        $this->_running   = true;

        // Figure out what we need to do. What was requested?
        list($action,$controller) = $this->route();

        // Let's make sure this request file exists.
        // This will throw an error if the page isn't found.
        list($action,$controllerObject) = $this->loadController($action,$controller);

        // This buffer catches the action's print.
        ob_start();

        // Call plugin _preDispatch method.
        for($i=0; $i < count($this->_plugin); $i++) {
            $this->_plugin[$i]->_preDispatch();
        }

        // What action are we going to run? This could have changed
        // during plugin _init() or _preDispatch().
        list($action,$controller) = $this->getAction();

        // Load/instantiate controller object
        list($action,$controllerObject) = $this->loadController($action,$controller);

        // Init controller
        $this->initController($controllerObject);

        // This is the meat and potatoes - run requested action
        $this->dispatchController($action,$controllerObject);

        // Call plugin post dispatch method.
        foreach(array_reverse($this->getPlugin()) as $plugin) {
            $plugin->_postDispatch();
        }

        // Everything comes out here.
        echo ob_get_clean();
        
        // First run returns false.
        return false;
    }

    /**
     * Try to require controller file. If the controller file is found,
     * then the action/method and respective controller object is returned.
     *
     * @param string $action
     * @param string $controller
     * @return array
     * @throws Spaph_Not_Found_Exception
     */
    private function loadController($action,$controller)
    {
        list($default_action,$default_controller) = $this->getAction();

        // Get controller ready for require_once.
        $controller = !$controller ? $default_controller : $controller;
        $controller_filename = $this->getControllerFileName($controller);

        // Initialize controllers if not set.
        if(!isset($this->_controller_obj[$controller_filename])) {
            $classname = ucfirst($controller) . 'Controller';
            require_once($controller_filename);
            $this->_controller_obj[$controller_filename] = new $classname;
        }

        // Check that the method exists.
        if( !method_exists($this->_controller_obj[$controller_filename], $action) || preg_match('/^_/',$action) )
            if( !method_exists($this->_controller_obj[$controller_filename], '__call') )
                throw new Spaph_Not_Found_Exception("The action you requested does not exist ($action.$controller).");

        // return action and controller object
        return array($action,$this->_controller_obj[$controller_filename]);
    }

    /**
     * Figure out the filename from the controller.
     *
     * @param string $controller
     * @return string
     * @throw Spaph_Not_Found_Exception
     */
    public function getControllerFileName($controller)
    {
        $filename = $this->getControllerDir() . DIRECTORY_SEPARATOR . ucfirst($controller).'Controller.php';

        if(!is_file($filename))
            throw new Spaph_Not_Found_Exception("Could not load page because it does not exist ($filename)");

        return $filename;
    }

    /**
     * To dispatch or not to dispatch. This can be set false to prevent Spaph
     * from dispatching the controller action.
     *
     * @param boolean $bool
     * @return boolean
     */
    public function dispatch($bool = null)
    {
        if(isset($bool))
            $this->_dispatch = $bool;

        return $this->_dispatch;
    }

    /**
     * This will actually call controllerObject->$action(), the requested action.
     * If forward is called, then this method will be called recursively. dispatchController
     * will not dispatch any methods beginning with an underscore.
     *
     * If Spaph::dispatch is set false then the action will not be dispatched.
     *
     * @param string $action
     * @param object $controllerObject
     * @return null
     */
    private function dispatchController($action,$controllerObject)
    {
        if($this->dispatch()) {
            // Here we go!
            $controllerObject->$action();
        }

        // Make a recursive call if necessary.
        if($this->_forward) {
            list($action,$controllerObject) = $this->_forward;
            $this->_forward = array(); // Stop recursion
            $this->dispatch(true); // In case it's false

            // Init controller
            $this->initController($controllerObject);

            // Run action
            $this->dispatchController($action,$controllerObject);
        }
    }

    /**
     * Make sure each controller has _init called only once.
     *
     * @param object $object
     * @return null
     */
    private function initController($object)
    {
        if(isset($this->_controller_init[get_class($object)]))
            return;

        $object->_init();
        $this->_controller_init[get_class($object)] = 1;
    }

    /**
     * Forward to another action.
     *
     * @param string $action
     * @param string $controller
     * @return null
     */
    public function forward()
    {
        list($default_action,$default_controller) = $this->getAction();

        $controller = $default_controller;
        $action     = null;

        if($num = func_num_args()) {
            if($num > 1) $controller = func_get_arg(1);
            if($num > 0) $action     = func_get_arg(0);

            $this->_forward = $this->loadController($action,$controller);
        }
    }

    /**
     * Set the header Location field. The first argument, $url, will be fed
     * to PHP's parse_url() function. The second argument is an array to
     * override any array key => val produced by PHP's parse_url($url). Refer
     * to parse_url() to find what keys $override might use.
     *
     * The $url should at least begin with '/' if the host name is excluded.
     *
     * @param string $url
     * @param array $override
     * @return null
     */
    public function redirect($url=null, $override=array())
    {
        if( (isset($url) && $url) || $override ) {
            $parts = array('scheme'=>'','user'=>'','pass'=>'','host'=>'',
                           'path'=>'','query'=>'','fragment'=>'');

            // Make sure $parse is an array.
            if( !$parse = parse_url($url) )
                $parse = array();

            // Make sure all parts are represented.
            $parse = array_merge($parts,$parse);

            // Prepend baseurl to path if no scheme or host in parse.
            $parse['path'] = $parse['scheme'] || $parse['host'] ? $parse['path'] : rtrim($this->getBaseUrl(),'/').'/'.ltrim($parse['path'],'/');

            // Override $parse with $override.
            $parse = array_merge($parse,$override);

            // Glue url
            $url  = $parse['scheme']   ? $parse['scheme']   : 'http';
            $url .= '://';
            $url .= $parse['user']     ? $parse['user'].':' : '';
            $url .= $parse['pass']     ? $parse['pass']     : '';
            $url .= $parse['host']     ? $parse['host']     : $this->getHost();
            $url .= $parse['path'];
            $url .= $parse['query']    ? '?'.$parse['query']    : '';
            $url .= $parse['fragment'] ? '#'.$parse['fragment'] : '';

            header('Location: '.$url);
            return $url;
        }
    }
} // end Spaph

/**
 * Spaph_Not_Found_Exception
 *
 * Could not find requested action.
 */
class Spaph_Not_Found_Exception extends Exception { }

/**
 * Spaph_DB_Connection_Exception
 *
 * Could not connect to database.
 */
class Spaph_DB_Connection_Exception extends Exception { }

